Skip to content

Improvement dashboard widget sizing - Expand dashboard widget sizing options - #456

Open
rafaelfoster wants to merge 4 commits into
ulsklyc:mainfrom
rafaelfoster:improvement-dashboard-widget-sizing
Open

Improvement dashboard widget sizing - Expand dashboard widget sizing options#456
rafaelfoster wants to merge 4 commits into
ulsklyc:mainfrom
rafaelfoster:improvement-dashboard-widget-sizing

Conversation

@rafaelfoster

Copy link
Copy Markdown
Contributor

Dashboards are a big deal for some users, and as a self-hosted solution, empowering users and their families to have more customization options is not just a nice-to-have feature—it’s a core expectation. Self-hosting is fundamentally about control. By offering modularity and deep personalization, we respect the user's sovereignty over their self-hosted environment, turning a static interface into a truly tailored home hub.

Summary:

  • Adds more dashboard widget size presets, including real one-row wide options: 3x1 and 4x1.
  • Keeps two-row wide options available separately: 3x2 and 4x2.
  • Preserves valid saved widget matrix sizes instead of collapsing them to smaller presets.
  • Updates all app locales and the dashboard sizing spec.
  • Fixes desktop grid row sizing so one-row widgets render at the intended reduced height instead of expanding to content-driven height.

@ulsklyc

ulsklyc commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Review: Dashboard widget sizing

Thanks for this — the motivation (sovereignty through customization) is exactly right for a self-hosted product, and the execution is clean: all 23 locales with real translations, SPEC in sync, a focused test, no constraint violations. Two things block the merge, though, and a third is a deliberate product decision we should make explicitly rather than reverse silently.

I reproduced the critical point with an isolated A/B test (below) — it's confirmed.


🔴 Blocker 1 — grid-auto-rows: 206px clips widget content silently

dashboard.css:108: minmax(132px, auto)206px.

The old auto component let the grid row grow with its content. The fixed 206px hard-caps every single-row track. Combined with two existing, deliberate rules, that turns into silent data loss:

  • .widget { overflow: hidden } (dashboard.css:222) — clips the overflow
  • .widget__body { overflow: visible } (dashboard.css:319) — no scroll, intentionally so because of [Bug] Scrolling on mobile safari results in blank screen #166 (blank-screen-on-scroll on iOS WebKit / Android Blink from nested scroll containers)

So the body can't scroll and the card clips — anything past 206px is gone: no scrollbar, no "+N more", no indicator.

Reproduced (isolated test, identical DOM, only grid-auto-rows varied; shopping widget at its default 2x1 with 3 lists / 21 rows):

before (minmax(132px, auto)) after (206px)
card height 697 px (grows) 206 px (capped)
body visible / actual 626 / 626 px 135 / 610 px
clipped rows 0 / 21 18 / 21
scrollable no (overflow-y: visible)

Why this isn't an edge case:

  • It hits default layouts, not just the new presets: shopping, notes, family, birthdays, weather, health, housekeeping all start single-row (dashboard.js:238-240).
  • It runs against the PR's own intent. The goal — bringing single-row widgets down to a reduced height — is valid (697px-tall single-row widgets look clumsy). But because of the no-scroll body decision, "reduced height" here means losing content, not rendering it more compactly.
  • The headline feature makes it worse: Wide (3x1) / Full row (4x1) add horizontal space, but the list stays vertically capped at 206px. So a "wide overview" shows less content, not more.

Fix options (by cleanliness):

  1. Cap instead of fix: grid-auto-rows: minmax(132px, 206px). Bounds the over-expansion (the actual concern) without decapitating content-heavy widgets. Solves most of it with no data loss — my preferred suggestion.
  2. Make the body scrollable again — but this collides head-on with [Bug] Scrolling on mobile safari results in blank screen #166; only with real-device regression testing.
  3. Offer single-row presets only for content-light widgets (weather/family), suppress them for list widgets.

🟡 Blocker 2 — the size control shows the wrong active state for matrix values

normalizeDashboardConfig now preserves arbitrary valid matrix values (3x3, 4x3, 2x4, …) instead of collapsing them to a preset (dashboard.js:283). But the UI only offers 8 buttons and highlights the nearest one via nearestPreset (dashboard.js:1198).

Result: a widget stored as 4x3 shows "Full (4x2)" as active (wrong), and any button click silently drops the 3rd row. Previously the UI was always truthful because normalize collapsed the value — now the stored value and the displayed state drift apart. The new test even locks in this reachable state (4x3 is preserved, test-dashboard.js:213).

Fix: either offer all 16 matrix values in the UI or keep collapsing on normalize. The current middle ground (preserve, but only show 8) is inconsistent.


🎛️ Product decision — doubling 4→8 presets: why 4 was chosen, and how 8 can still work

This is not a bug — it's a deliberate change of direction that overwrites the removed comment (dashboard.js, "≤4-choices rule, Critique P2"). It deserves an explicit call. The full reasoning behind the original limit of 4:

1. Decision cost scales with options × widgets. Size isn't an isolated choice — it's made per widget, across up to 13 widgets. At 4 options the choice is a reflex (Hick's law: decision time grows with the number of options, and the cumulative load across 13 tiles is real). At 8, the search space per tile doubles and Customize mode tips from "make it pretty quickly" into "reason about every tile individually." For a family audience touching the dashboard on a phone while cooking, that's friction in exactly the wrong place.

2. It collides with two documented product principles:

  • "Power without noise" — feature depth is revealed through navigation, not laid out up front. 8 flat options are the opposite.
  • Anti-reference "overloaded feature dashboard" — "Power is revealed on demand, not laid out in one giant grid." A size picker with 8 shapes per tile is a micro-cosm of exactly that pattern.

3. Curation is the value here. Four shapes (compact / tall / standard / wide) cover the real layout needs; the other 4 are mostly variants almost nobody deliberately reaches for but everybody has to look at. "Fewer, but the right ones" is a feature in a family tool, not a shortcoming.

But: your sovereignty argument is legit, and there are users who want exactly this control. The conflict isn't 4-vs-8, it's a flat wall vs. a structured choice. Hence the compromise:

Keep the 8 presets — but group them in the UI into two visually separated rows: one row for "1 row tall" (1x1 · 2x1 · 3x1 · 4x1) and below it "2 rows tall" (1x2 · 2x2 · 3x2 · 4x2). That turns "8 things I have to compare" into two times "4 along one clear axis (width)" — the cognitive load stays around 4 per decision, because height pre-sorts the rows. Progressive disclosure with no loss of capability. It satisfies both goals: full control and "power without noise."

If you go that route, Blocker 2 should be fixed alongside it (then every offered value is also a stored value — no more nearestPreset lie).


Verdict

Request changes. Blocker 1 (clipping) is the hard stopper and it's proven — please resolve via fix option 1 (minmax(132px, 206px)). Fix Blocker 2 alongside. The preset count is your call; if 8, please group rather than flatten. Once that's in, this is a strong and welcome contribution.

Reproduction: seeded DB, single dashboard, only the grid-auto-rows value swapped at runtime on the same rendered DOM; measurements via getBoundingClientRect / scrollHeight. Before/after screenshots available on request.

@rafaelfoster

Copy link
Copy Markdown
Contributor Author

Thanks @ulsklyc for the detailed review and the reproduction data.

I addressed the blockers in cb2291a:

  • Fixed the row-height clipping by changing the desktop grid from a fixed 206px row to minmax(132px, 206px), matching your suggested fix.
  • Fixed the size picker state drift by removing the “nearest preset” active-state behavior. Stored hidden matrix values are now normalized to the nearest offered preset instead, on both the
    client and preferences API, so values like 4x3 become 4x2 and the UI remains truthful.
  • Kept the 8-preset direction, but grouped the picker into two rows: 1 row presets and 2 rows presets. That keeps the sovereignty/customization benefit while reducing the flat 8-option
    decision cost.
  • Updated all locales, SPEC, and the dashboard test coverage.

Validation passed:

  • test:dashboard
  • test:preferences-navigation
  • test:preferences-weather
  • test:frontend-audit

@ulsklyc

ulsklyc commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Re-review of the revision (cb2291a5d)

Thanks for taking the review seriously — two of the three points are cleanly resolved. On the third I owe you a correction: the fix I originally recommended was wrong, and it doesn't solve the blocker. Details below.

✅ Grouped presets — resolved exactly as discussed

WIDGET_SIZE_PRESET_GROUPS splits the eight presets into a "One row" row (1x1·2x1·3x1·4x1) and a "Two rows" row (1x2·2x2·3x2·4x2), each rendered as its own role="group" with an aria-label. That's the compromise: full control, but ~4 choices along one axis instead of a flat wall of 8. Both new label keys are present across all 23 locales. 👍

✅ Size-control drift — resolved

nearestPreset is gone, replaced by normalizeWidgetSize, which collapses any stored size to one of the 8 presets (WIDGET_SIZE_OPTIONS is now just those 8, not the full 16-cell matrix). activeSize therefore reflects the exact stored value — no more "highlights 4x2 but stores 4x3" mismatch. 👍

❌ Silent clipping — still present (and this one is on me)

You applied my suggestion verbatim: grid-auto-rows: minmax(132px, 206px). I was wrong to recommend it. I claimed it would "bound the over-expansion without decapitating content-heavy widgets" — but the max is a fixed 206px, so the track caps at 206px exactly like the previous fixed 206px value and still clips anything taller, silently (no scroll, per #166).

I reproduced it in isolation (a standalone page replicating the exact dashboard.css structure — grid + .widget-wrapper + .widget{overflow:hidden} + .widget__body{overflow:visible} — with a 2×1 shopping widget of 18 items):

before minmax(132px, auto) this PR minmax(132px, 206px)
card height 587 px (grows) 206 px (capped)
body visible / actual 546 / 546 px 165 / 530 px
items clipped 0 / 18 13 / 18
scrollable no

So a content-heavy single-row widget still loses 13 of 18 rows with no scrollbar and no indicator. minmax(132px, 206px) only reduces whitespace for sparse widgets; for full ones it's the same decapitation as fixed 206px.

The root is the triangle I flagged in the first review: single-row-short + no body scroll (#166) + overflow:hidden makes clipping unavoidable — no grid-auto-rows value squares that circle. My "option 1" never could; the workable direction is closer to my original "option 3."

A fix that actually holds (not another CSS tweak):

  • Preferred: in single-row sizes, cap the number of rendered items and reuse the existing "+N more" row (Shopping already computes moreCount; Tasks/Calendar/Notes can do the same). Nothing is lost — a single-row widget becomes an honest at-a-glance summary that links to the full module.
  • Alternative: add a bottom fade/gradient as a "more below" affordance and treat single-row explicitly as a summary.
  • Making the body scrollable stays off the table (reintroduces [Bug] Scrolling on mobile safari results in blank screen #166).

Also: needs a rebase

The branch is now conflictingmain has moved to v1.7.x while this PR sits on an older base. It'll need a rebase before it can merge regardless.


Net: presets grouping and the size-control drift are done and good. The clipping blocker is still open — and that's because my recommended CSS cap can't fix it. Sorry for sending you down that path. The item-cap + "+N more" approach is the one that resolves it without losing content.

@ulsklyc

ulsklyc commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Thanks for this, Rafael - the wide one-row sizes are a genuinely useful addition and the locale coverage is thorough. A few things need addressing before it can go in, mostly because main has moved since you opened the PR.

The main point: this collides with #547.
Since you branched, #547 (persist variable-length widget layouts) landed and took the server in the opposite direction. normalizeWidgetConfig in server/routes/preferences.js is now a strict validator that returns null (rejecting the whole config) for any size not in VALID_WIDGET_SIZES, and the old defaultWidgetSize helper was removed. So this isn't a mechanical rebase - the two changes have opposite intents (keep any saved size vs. normalize down to a preset set). It's worth deciding which direction we want before reworking the server hunk.

Blocking

  1. Data-loss risk for existing users. If VALID_WIDGET_SIZES shrinks to the 8 presets while the strict validator stays, a stored legacy size (e.g. 4x4) makes normalizeWidgetConfig return null: saving any dashboard change returns 400, and on read parseWidgetConfig (normalizeWidgetConfig(parsed) ?? []) silently drops the saved layout. The collapseMatrixSizeToPreset logic needs to be wired into the current for...of validator so those values collapse instead of being rejected.

  2. Mini-grid preview can't tell the new widths apart. renderSizeMiniGridCells renders a fixed 2x2 grid with Math.min(cols, 2) / Math.min(rows, 2), so 2x1/3x1/4x1 all look identical and 2x2/3x2/4x2 all look identical - 6 of the 8 buttons are visual duplicates. The preview needs a wider base raster (e.g. 4x2) so each button shows its actual shape.

Should fix

  • The desktop grid-auto-rows: minmax(132px, 206px) cap clips content on 2-row widgets. Widgets are overflow: hidden with no internal scroll, so anything past ~432px is cut off instead of growing as before. Reasonable for 1-row sizes (the actual bug), but risky for the default 1x2 list widgets (tasks/calendar/budget). Could you scope the cap to 1-row spans, or confirm the tallest lists still fit?
  • Server-side tests for the new normalization (1x3 -> 1x2, 4x4 -> 4x2, unknown -> default) in test/test-preferences-routes.js. That path is the riskiest part and currently only the client is covered.
  • A ## [Unreleased] entry in CHANGELOG.md.

Happy to re-review once the server side is reworked against current main. Thanks again for the contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants